<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="VylSB" id="VylSB"><span data-lake-id="ub56f8ecb" id="ub56f8ecb">典型回答</span></h1>
  <p data-lake-id="u2dff3787" id="u2dff3787"><br></p>
  <p data-lake-id="u9d240f4a" id="u9d240f4a"><span data-lake-id="uc73626c5" id="uc73626c5">InnoDB中的REPEATABLE READ这种隔离级别通过间隙锁+MVCC解决了大部分的幻读问题，但是并不是所有的幻读都能解读，想要彻底解决幻读，需要使用Serializable的隔离级别。</span></p>
  <p data-lake-id="ubc67a65f" id="ubc67a65f"><br></p>
  <p data-lake-id="u7bd86146" id="u7bd86146"><span data-lake-id="u644f7852" id="u644f7852">RR中，通过间隙锁解决了部分当前读的幻读问题，通过增加间隙锁将记录之间的间隙锁住，避免新的数据插入。</span></p>
  <p data-lake-id="u942dbb41" id="u942dbb41"><span data-lake-id="ua85326ae" id="ua85326ae">​</span><br></p>
  <p data-lake-id="u13db8c1b" id="u13db8c1b"><span data-lake-id="uf857923a" id="uf857923a">RR中，通过MVCC机制的，解决了快照读的幻读问题，RR中的快照读只有第一次会进行数据查询，后面都是直接读取快照，所以不会发生幻读。</span></p>
  <p data-lake-id="u1c382385" id="u1c382385"><span data-lake-id="ufe9fb250" id="ufe9fb250">​</span><br></p>
  <p data-lake-id="u5967c0f1" id="u5967c0f1"><strong><span data-lake-id="u6b3df912" id="u6b3df912">但是，如果两个事务，事务1先进行快照读，然后事务2插入了一条记录并提交，再在事务1中进行update新插入的这条记录是可以更新出成功的，这就是发生了幻读。</span></strong></p>
  <p data-lake-id="u2d2f82e6" id="u2d2f82e6"><strong><span data-lake-id="uefa9c1f2" id="uefa9c1f2">​</span></strong><br></p>
  <p data-lake-id="u65caacee" id="u65caacee"><strong><span data-lake-id="u9cc0f824" id="u9cc0f824">还有一种场景，如果两个事务，事务1先进行快照读，然后事务2插入了一条记录并提交，在事务1中进行了当前读之后，再进行快照读也会发生幻读。</span></strong></p>
  <p data-lake-id="uf75f6af6" id="uf75f6af6"><br></p>
  <h1 data-lake-id="Xat6O" id="Xat6O"><span data-lake-id="u454453f1" id="u454453f1">扩展知识</span></h1>
  <p data-lake-id="u781f03b2" id="u781f03b2"><br></p>
  <h2 data-lake-id="KJQdq" id="KJQdq"><span data-lake-id="u7c163301" id="u7c163301">MVCC解决幻读</span></h2>
  <p data-lake-id="u3d692cbb" id="u3d692cbb"><span data-lake-id="u0ff65975" id="u0ff65975">MVCC，是Multiversion Concurrency Control的缩写，翻译过来是多版本并发控制</span><span data-lake-id="ube71cccb" id="ube71cccb">，和数据库锁一样，他也是一种并发控制的解决方案。它主要用来解决读-写并发的情况。</span></p>
  <p data-lake-id="uc383cfba" id="uc383cfba"><span data-lake-id="u66571351" id="u66571351">​</span><br></p>
  <p data-lake-id="u2a1c9cab" id="u2a1c9cab"><span data-lake-id="ucf72be9b" id="ucf72be9b">​</span><br></p>
  <p data-lake-id="u2471583e" id="u2471583e"><span data-lake-id="u8e922da5" id="u8e922da5">我们知道，</span><span data-lake-id="ua4c84a91" id="ua4c84a91">在MVCC中有两种读，一种是快照读、一种是当前读。</span></p>
  <p data-lake-id="u4f5e800c" id="u4f5e800c"><span data-lake-id="u83bbe81f" id="u83bbe81f">​</span><br></p>
  <p data-lake-id="u9f65fe01" id="u9f65fe01"><span data-lake-id="u848dde0f" id="u848dde0f">所谓快照读，就是读取的是快照数据，即快照生成的那一刻的数据，像我们常用的普通的SELECT语句在不加锁情况下就是快照读。</span></p>
  <p data-lake-id="u9f42e8d8" id="u9f42e8d8"><span data-lake-id="u1dd43d85" id="u1dd43d85">​</span><br></p>
  <p data-lake-id="u67578490" id="u67578490"><span data-lake-id="u02ac9445" id="u02ac9445">SELECT * FROM xx_table WHERE ...</span></p>
  <p data-lake-id="u33db32cb" id="u33db32cb"><span data-lake-id="u93e1fd60" id="u93e1fd60">​</span><br></p>
  <p data-lake-id="u5003610e" id="u5003610e"><span data-lake-id="u9c25df86" id="u9c25df86">在 RC 中，每次读取都会重新生成一个快照，总是读取行的最新版本。</span></p>
  <p data-lake-id="ubd0d68ad" id="ubd0d68ad"><span data-lake-id="ufd072a11" id="ufd072a11">在 RR 中，快照会在事务中第一次SELECT语句执行时生成，只有在本事务中对数据进行更改才会更新快照。</span></p>
  <p data-lake-id="u8968d74a" id="u8968d74a"><span data-lake-id="uf7eaf55f" id="uf7eaf55f">​</span><br></p>
  <p data-lake-id="u70ff41ed" id="u70ff41ed"><span data-lake-id="u0758c311" id="u0758c311">那么也就是说，</span><span data-lake-id="uc494395c" id="uc494395c">如果在RR下，一个事务中的多次查询，是不会查询到其他的事务中的变更内容的，所以，也就是可以解决幻读的。</span></p>
  <p data-lake-id="uec1929e4" id="uec1929e4"><span data-lake-id="ue77b7ef3" id="ue77b7ef3">​</span><br></p>
  <p data-lake-id="u1e326e7a" id="u1e326e7a"><span data-lake-id="ud1ba2ba2" id="ud1ba2ba2">如果我们把事务隔离级别设置为RR，那么因为有了MVCC的机制，就能解决幻读的问题：</span></p>
  <p data-lake-id="u85bc9f60" id="u85bc9f60"><span data-lake-id="u68e2f42c" id="u68e2f42c">​</span><br></p>
  <p data-lake-id="u2bd29cfe" id="u2bd29cfe"><span data-lake-id="u86bfb694" id="u86bfb694">有这样一张表：</span></p>
  <p data-lake-id="u4492f499" id="u4492f499"><span data-lake-id="u3f70ce93" id="u3f70ce93">​</span><br></p>
  <pre lang="java"><code>
CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT,
    gmt_create DATETIME NOT NULL,
    age INT NOT NULL,
    name VARCHAR(16) NOT NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

INSERT INTO users(gmt_create,age,name) values(now(),18,'Hollis');
INSERT INTO users(gmt_create,age,name) values(now(),28,'HollisChuang');
INSERT INTO users(gmt_create,age,name) values(now(),38,'Hollis666');
</code></pre>
  <p data-lake-id="u31882744" id="u31882744"><br></p>
  <p data-lake-id="ueca20321" id="ueca20321"><span data-lake-id="ud9794e1b" id="ud9794e1b">执行如下事务时序：</span></p>
  <p data-lake-id="u1c965e6f" id="u1c965e6f"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1672142200253-401443f6-478e-4fbd-a570-2ca84bcf5f67.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_45%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u06af5c54" id="u06af5c54"><br></p>
  <p data-lake-id="u6de820d2" id="u6de820d2"><span data-lake-id="u584f8dae" id="u584f8dae">可以看到，同一个事务中的两次查询结果是一样的，就是在RR级别下，因为有快照读，所以第二次查询其实读取的是一个快照数据。</span></p>
  <p data-lake-id="u87f06dbc" id="u87f06dbc"><span data-lake-id="ubc7dc0bf" id="ubc7dc0bf">​</span><br></p>
  <h2 data-lake-id="kvO8r" id="kvO8r"><span data-lake-id="udb63c7ee" id="udb63c7ee">间隙锁与幻读</span></h2>
  <p data-lake-id="ued9452d1" id="ued9452d1"><br></p>
  <p data-lake-id="ub2430b81" id="ub2430b81"><span data-lake-id="ua906563c" id="ua906563c">上面我们讲过了MVCC能解决RR级别下面的快照读的幻读问题，那么</span><span data-lake-id="uadd06d2d" id="uadd06d2d">当前读下面的幻读问题怎么解决呢？</span></p>
  <p data-lake-id="uc38e8ebf" id="uc38e8ebf"><span data-lake-id="ud4e64548" id="ud4e64548">​</span><br></p>
  <p data-lake-id="u15fd7bfc" id="u15fd7bfc"><span data-lake-id="u056617c8" id="u056617c8">当前读就是读取最新数据，所以，加锁的 SELECT，或者对数据进行增删改都会进行当前读，比如：</span></p>
  <p data-lake-id="u5ce94465" id="u5ce94465"><span data-lake-id="u794ba574" id="u794ba574">​</span><br></p>
  <pre lang="java"><code>
SELECT * FROM xx_table LOCK IN SHARE MODE;

SELECT * FROM xx_table FOR UPDATE;

INSERT INTO xx_table ...

DELETE FROM xx_table ...

UPDATE xx_table ...
</code></pre>
  <p data-lake-id="ufa26eebe" id="ufa26eebe"><span data-lake-id="u7afc41f6" id="u7afc41f6">​</span><br></p>
  <p data-lake-id="ua90c0aa4" id="ua90c0aa4"><span data-lake-id="u27612f3e" id="u27612f3e">举一个下面的例子：</span></p>
  <p data-lake-id="u9be4ccb7" id="u9be4ccb7"><span data-lake-id="ud4ca42b9" id="ud4ca42b9"><br></span><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1672142247819-ebebed97-c9a1-41f8-b8d5-53201bfeb3d1.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_45%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u79e62d36" id="u79e62d36"><br></p>
  <p data-lake-id="ub62e43fc" id="ub62e43fc"><span data-lake-id="u92f139b9" id="u92f139b9">向上面这种情况，在RR的级别下，当我们使用SELECT … FOR UPDATE的时候，会进行加锁，不仅仅会对行记录进行加锁，还会对记录之间的间隙进行加锁，这就叫做间隙锁。</span></p>
  <p data-lake-id="u05540d09" id="u05540d09"><span data-lake-id="ua78bf5a9" id="ua78bf5a9">​</span><br></p>
  <p data-lake-id="ucf0b1dc8" id="ucf0b1dc8"><span data-lake-id="u8e3b1e89" id="u8e3b1e89">​</span><br></p>
  <p data-lake-id="u15b672af" id="u15b672af"><span data-lake-id="ub1f73c7e" id="ub1f73c7e">因为记录之间的间隙被锁住了，所以事务2的插入操作就被阻塞了，一直到事务1把锁释放掉他才能执行成功。</span></p>
  <p data-lake-id="u8d762dfa" id="u8d762dfa"><span data-lake-id="ue83f6fd7" id="ue83f6fd7">​</span><br></p>
  <p data-lake-id="u736a3e35" id="u736a3e35"><span data-lake-id="u6e1001e0" id="u6e1001e0">因为事务2无法插入数据成功，所以也就不会存在幻读的现象了。所以，在RR级别中，通过加入间隙锁的方式，就避免了幻读现象的发生。</span></p>
  <h2 data-lake-id="MPlOV" id="MPlOV"><span data-lake-id="u937593e5" id="u937593e5"><br></span><span data-lake-id="uc0572f2d" id="uc0572f2d">解决不了的幻读</span></h2>
  <p data-lake-id="ub1ef2a57" id="ub1ef2a57"><span data-lake-id="ue6d7dfb8" id="ue6d7dfb8">​</span><br></p>
  <p data-lake-id="u123d92c2" id="u123d92c2"><span data-lake-id="u5c98e5d3" id="u5c98e5d3">前面我们介绍了快照读（无锁查询）和当前读（有锁查询）下是如何解决幻读的问题的，但是，上面的例子就是幻读的所有情况了吗？显然并不是。</span></p>
  <p data-lake-id="u5879e7ed" id="u5879e7ed"><span data-lake-id="u78544a2e" id="u78544a2e">​</span><br></p>
  <p data-lake-id="u8789f381" id="u8789f381"><span data-lake-id="u9747a09e" id="u9747a09e">我们说MVCC只能解决快照读的幻读，那如果在一个事务中发生了当前读，并且在另一个事务插入数据前没来得及加间隙锁的话，会发生什么呢？</span></p>
  <p data-lake-id="u64fc7218" id="u64fc7218"><span data-lake-id="u8d255b61" id="u8d255b61">​</span><br></p>
  <p data-lake-id="ue5e96b85" id="ue5e96b85"><span data-lake-id="ud1514171" id="ud1514171">那么，我们稍加修改一下上面的SQL代码，通过当前读的方式进行查询数据：</span></p>
  <p data-lake-id="u0aff03c3" id="u0aff03c3"><span data-lake-id="u11356a13" id="u11356a13">​</span><br></p>
  <p data-lake-id="ub588d63e" id="ub588d63e"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1672142331125-467021b3-992f-4a2a-a563-36eed630409c.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_46%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u8a43aa9d" id="u8a43aa9d"><br></p>
  <p data-lake-id="u9469c24f" id="u9469c24f"><span data-lake-id="ud0fd16ea" id="ud0fd16ea">在上面的例子中，在事务1中，我们并没有在事务开启后立即加锁，而是进行了一次普通的查询，然后事务2插入数据成功之后，再通过事务1进行了2次查询。</span></p>
  <p data-lake-id="uf0383e26" id="uf0383e26"><span data-lake-id="u9db86f2d" id="u9db86f2d">​</span><br></p>
  <p data-lake-id="ub4d89c9f" id="ub4d89c9f"><span data-lake-id="u3580ed52" id="u3580ed52">我们发现，事务1后面的两次查询结果完全不一样，没加锁的情况下，就是快照读，读到的数据就和第一次查询是一样的，就不会发生幻读。但是第二次查询加了锁，就是当前读，那么读取到的数据就有其他事务提交的数据了，就发生了幻读。</span></p>
  <p data-lake-id="u37443bfb" id="u37443bfb"><span data-lake-id="uc65af9ac" id="uc65af9ac">​</span><br></p>
  <p data-lake-id="u7a025126" id="u7a025126"><span data-lake-id="u725b11eb" id="u725b11eb">那么，如果你理解了上面的这个例子，并且你也理解了当前读的概念，那么你很容易就能想到，下面的这个CASE其实也是会发生幻读的：</span></p>
  <p data-lake-id="u19adfcb7" id="u19adfcb7"><span data-lake-id="uda480dd9" id="uda480dd9">​</span><br></p>
  <p data-lake-id="u14384ba5" id="u14384ba5"><img src="https://cdn.nlark.com/yuque/0/2022/png/5378072/1672142346476-c06e2549-837d-4d61-b3cb-d743f9131941.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_45%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="u0fb66390" id="u0fb66390"><br></p>
  <p data-lake-id="ue4824bda" id="ue4824bda"><span data-lake-id="u30c20ec4" id="u30c20ec4">这里发生幻读的原理，和上面的例子其实是一样的，那就是MVCC只能解决快照读中的幻读问题，而对于当前读（SELECT FOR UPDATE、UPDATE、DELETE等操作）还是会产生幻读的现象的。即，在同一个事务里面，如果既有快照读，又有当前读，那是会产生幻读的、</span></p>
  <p data-lake-id="u255dae27" id="u255dae27"><br></p>
  <p data-lake-id="uc2f0fff7" id="uc2f0fff7"><span data-lake-id="u45b2ccef" id="u45b2ccef">UPDATE语句也是一种当前读，所以它是可以读到其他事务的提交结果的。</span></p>
  <p data-lake-id="u43830afe" id="u43830afe"><span data-lake-id="u74f89072" id="u74f89072">​</span><br></p>
  <p data-lake-id="u28ce62a7" id="u28ce62a7"><span data-lake-id="u8b9f44ed" id="u8b9f44ed">为什么事务1的最后一次查询和倒数第二次查询的结果也不一样呢？</span></p>
  <p data-lake-id="u0511b40b" id="u0511b40b"><span data-lake-id="ud797f030" id="ud797f030">​</span><br></p>
  <p data-lake-id="ud1b9c51b" id="ud1b9c51b"><span data-lake-id="u6e837896" id="u6e837896">是因为根据快照读的定义，在RR中，如果本事务中发生了数据的修改，那么就会更新快照，那么最后一次查询的结果也就发生了变化。</span></p>
  <p data-lake-id="ufd12886a" id="ufd12886a"><span data-lake-id="u86c1381b" id="u86c1381b">​</span><br></p>
  <h2 data-lake-id="Mr5ws" id="Mr5ws"><span data-lake-id="u9386163b" id="u9386163b">如何避免幻读</span></h2>
  <p data-lake-id="u07740bc4" id="u07740bc4"><br></p>
  <p data-lake-id="u4bcc6ab9" id="u4bcc6ab9"><span data-lake-id="u4a002be8" id="u4a002be8">那么了解了幻读的解决场景，以及不能解决的几个CASE之后，我们来总结一下该如何解决幻读的问题呢？</span></p>
  <p data-lake-id="u0ef4923f" id="u0ef4923f"><span data-lake-id="ub5e023d9" id="ub5e023d9">​</span><br></p>
  <p data-lake-id="u1feffb5e" id="u1feffb5e"><span data-lake-id="u4bd2e57f" id="u4bd2e57f">首先，如果想要彻底解决幻读的问题，在InnoDB中只能使用Serializable这种隔离级别。</span></p>
  <p data-lake-id="u98f1e496" id="u98f1e496"><img src="https://cdn.nlark.com/yuque/0/2022/jpeg/5378072/1672142361498-f08c8482-9c80-4328-b9a0-9ceb5e8c2b00.jpeg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_68%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="ucfaeb04e" id="ucfaeb04e">图源：MySQL 8.0 Reference Manual</span></p>
  <p data-lake-id="u914c136a" id="u914c136a"><span data-lake-id="u31538852" id="u31538852">​</span><br></p>
  <p data-lake-id="u374825c3" id="u374825c3"><span data-lake-id="u1a189b0e" id="u1a189b0e">那么，如果想在一定程度上解决或者避免发生幻读的话，使用RR也可以，但是RC、RU肯定是不行的。</span></p>
  <p data-lake-id="uf86c7404" id="uf86c7404"><span data-lake-id="u6b413b80" id="u6b413b80">​</span><br></p>
  <p data-lake-id="u82d6a6ba" id="u82d6a6ba"><span data-lake-id="ud1325380" id="ud1325380">在RR级别中，能使用快照读（无锁查询）的就使用快照读，这样不仅可以减少锁冲突，提升并发度，而且还能避免幻读的发生。</span></p>
  <p data-lake-id="ua4d5caa5" id="ua4d5caa5"><span data-lake-id="u1425a355" id="u1425a355">​</span><br></p>
  <p data-lake-id="ua45e3db8" id="ua45e3db8"><span data-lake-id="udce57741" id="udce57741">那么，如果在并发场景中，一定要加锁的话怎么办呢？那就一定要在事务一开始就立即加锁，这样就会有间隙锁，也能有效的避免幻读的发生。</span></p>
  <p data-lake-id="u9f84f398" id="u9f84f398"><span data-lake-id="u48cbed67" id="u48cbed67">但是需要注意的是，间隙锁是导致死锁的一个重要根源~所以，用起来也需要慎重。</span></p>
 </body>
</html>